import { ShiosCacheStorage } from './cache';

const shiosCaches = new ShiosCacheStorage();
//import { ShiosResponse, ShiosRequestInit, URLParams } from './type';
const httpStatus: Record<number, string> = {
  200: 'OK',
  201: 'Created',
  204: 'No Content',
  400: 'Bad Request',
  401: 'Unauthorized',
  403: 'Forbidden',
  404: 'Not Found',
  408: 'Request Time-out',
  409: 'Conflict',
  500: 'Internal Server Error',
};
enum MethodType {
  OPTIONS = 'OPTIONS',
  GET = 'GET',
  HEAD = 'HEAD',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE',
  TRACE = 'TRACE',
  CONNECT = 'CONNECT',
}
//content-disposition: attachment; filename=42%___________-__12208071526.txt; filename*=UTF-8''42%25%E4%B8%89%E6%B0%AF%E5%BC%82%E6%B0%B0%E5%B0%BF%E9%85%B8%E5%8F%AF%E6%B9%BF%E6%80%A7%E7%B2%89%E5%89%82-%E6%B5%8B%E8%AF%9512208071526.txt
function getFileName(disposition: string) {
  let dispositions = disposition.split('; ');
  if (dispositions[0] != 'attachment') {
    return '';
  }
  if (dispositions[2].startsWith(`filename*=UTF-8''`)) {
    return decodeURIComponent(dispositions[2].substring(17));
  } else if (dispositions[1].startsWith('filename=')) {
    return decodeURIComponent(dispositions[1].substring(9));
  }
}
type Next = (response: Response, next?: Next) => unknown;
interface ChainFunc {
  (response: Response, next?: ChainFunc): unknown;
}

async function getText(response: Response, next?: ChainFunc) {
  let contentType = response.headers.get('content-type');
  if (contentType?.includes('text')) {
    return response.text();
  } else {
    return next?.(response);
  }
}
async function getJson(response: Response, next?: ChainFunc) {
  let contentType = response.headers.get('content-type');
  if (contentType?.includes('json')) {
    return response.json();
  } else {
    return next?.(response);
  }
}

async function getBlob(response: Response, next?: ChainFunc) {
  let disposition = response.headers.get('content-disposition');

  if (disposition) {
    let fileName = getFileName(disposition);
    if (fileName) {
      let blob = await response.blob();

      const tmpLink = document.createElement('a');
      const objectUrl = URL.createObjectURL(blob);
      tmpLink.href = objectUrl;
      tmpLink.download = fileName;
      // document.body.appendChild(tmpLink); // 如果不需要显示下载链接可以不需要这行代码
      tmpLink.click();
      URL.revokeObjectURL(objectUrl);
    }
  } else {
    return next?.(response);
  }
}

/**
 * 网络http请求
 */
export class Shios {
  private _chain: Array<ChainFunc> = [];
  private _token?: string;

  baseUrl?: string;
  headers: Record<string, string> = { 'Content-Type': 'application/json' };
  /** 超时时间，单位 ms */
  timeout?: number;
  isCache = false;

  public get token(): string | undefined {
    return this._token;
  }
  public set token(value: string | undefined) {
    if (value) {
      this.headers.authorization = `Bearer ${value}`;
    } else {
      delete this.headers.authorization;
    }
    this._token = value;
  }
  private _isDebug = false;
  debug: (...data: any[]) => void = () => {};
  requestAfter?: (response: ShiosResponse<unknown>, ...args: any[]) => void;
  exceptionHandler?: (response: ShiosResponse<unknown>, ...args: any[]) => void;
  failureHandler?: (
    response: ShiosResponse<unknown>,
    pathname: string,
    init: ShiosRequestInit
  ) => void;
  public get isDebug() {
    return this._isDebug;
  }
  public set isDebug(value: boolean) {
    if (value) {
      this.debug = console.log.bind(console);
    }
    this._isDebug = value;
  }
  /**
   * 私有构造函数
   */
  private constructor() {
    //this.use(getBlob);
    this.use(getJson);
    this.use(getText);
    // this.use(this.getError);
  }
  static instance: Shios;
  static getInstance() {
    if (!Shios.instance) {
      Shios.instance = new Shios();
    }
    return Shios.instance;
  }
  /**
   *
   * @param pathname apipath api路径
   * @param { ShiosRequestInit} init { path, search }
   * @return {Promise<ShiosResponse<T>>} Promise<T>
   */
  get<T>(
    pathname: string,
    init: ShiosRequestInit = {}
  ): Promise<ShiosResponse<T>> {
    init.method = MethodType.GET;
    return this.requst<T>(pathname, init);
  }
  /**
   *
   * @param {string}pathname apipath api路径
   * @param { ShiosRequestInit}init
   */
  post<T>(pathname: string, init: ShiosRequestInit = {}) {
    init.method = MethodType.POST;
    return this.requst<T>(pathname, init);
  }
  put<T>(pathname: string, init: ShiosRequestInit = {}) {
    init.method = MethodType.PUT;
    return this.requst<T>(pathname, init);
  }
  patch<T>(pathname: string, init: ShiosRequestInit = {}) {
    init.method = MethodType.PATCH;
    return this.requst<T>(pathname, init);
  }
  delete<T>(pathname: string, init: ShiosRequestInit = {}) {
    init.method = MethodType.DELETE;
    return this.requst<T>(pathname, init);
  }
  async download(pathname: string, init: ShiosRequestInit = {}) {
    this._chain.unshift(getBlob);
    var result = await this.get(pathname, init);
    this._chain.shift();
    return result;
  }

  async requst<T>(
    pathname: string,
    init: ShiosRequestInit
  ): Promise<ShiosResponse<T>> {
    if (init.data) {
      init.body = JSON.stringify(init.data);
    }

    init.headers = this.setHeaders(init.headers);
    this.debug('init', init);
    let res: ShiosResponse<T> = {
      url: '',
      ok: false,
    };

    if (typeof uni != 'undefined' && typeof uni.request == 'function') {
      res = await this.uniRequest(pathname, init);
    } else {
      try {
        let response = await this.fetchAsync(pathname, init);
        this.debug(response);
        res = {
          ok: response.ok ?? response.status < 400,
          url: response.url,
          status: response.status ?? 0,
          statusText: response.statusText || httpStatus[response.status],
          headers: {},
        };
        response.headers.forEach((v, k) => {
          res!.headers![k] = v;
        });
        this.index = 0;

        if (res.ok) {
          res.data = (await this.handleResponse(response)) as T;
        } else {
          res.error = (await this.handleResponse(response)) as any;
          res.requestData = init.data;
        }
      } catch (e) {
        if (e instanceof TypeError && e.message == 'Failed to fetch') {
          console.error('response', e.message);
          res.error = e.message;
          // throw new URIError(`${res.method} ${res.url} Failed`);
        } else {
          throw e;
        }
      }
    }

    if (this.requestAfter) {
      this.requestAfter(res);
    }
    this.debug('res', res);
    if (!res.ok) {
      res.requestData = init.data;
      console.error('res:', res);

      if (this.failureHandler) {
        this.failureHandler(res, pathname, init);
        //res = await this.requst(pathname, init);
      }
    }
    // console.error('request', init);
    // console.error('response', res);
    return res;
    // switch (res.status) {
    //   case 400:
    //     throw new BadRequestError();
    //   case 403:
    //     throw new UnauthorizedError();
    //   case 404:
    //     throw new NotFoundError();
    //   case 409:
    //     throw new ConflictError();
    //   case 500:
    //     var err = new ProblemError();
    //     err.cause = res as any;
    //     throw new ProblemError();
    //   default:
    //     return res;
    // }
  }
  private async fetchAsync(pathname: string, init: ShiosRequestInit) {
    let response;
    const url = this.CombinedURL(pathname, init);
    const controllerName = pathname.split(/[/?]/, 1)[0];

    if (this.isCache && init?.method == 'GET') {
      response = await caches.match(url);
      if (response) {
        console.log('cached');
        return response;
      }
    }
    if (this.timeout) {
      response = await Promise.race([
        fetch(url, init),
        new Promise<Response>((resolve, reject) => {
          setTimeout(() => {
            reject(new Error('超时'));
          }, this.timeout);
        }),
      ]);
    } else {
      response = await fetch(url, init);
    }
    if (this.isCache) {
      switch (init?.method?.toUpperCase()) {
        case 'GET':
        case 'HEAD':
          (await caches.open(controllerName)).put(url, response.clone());
          break;
        case 'POST':
          await (
            await caches.open(controllerName)
          ).delete(url, {
            ignoreSearch: true,
          });
          break;
        case 'PUT':
        case 'DELETE':
          caches.delete(controllerName);
          break;
        default:
          break;
      }
    }

    return response;
  }
  clearCache() {
    caches;
  }
  private async uniRequest<T>(
    pathname: string,
    init: ShiosRequestInit
  ): Promise<ShiosResponse<T>> {
    let response: ShiosResponse<T> | undefined;
    const url = this.CombinedURL(pathname, init);
    if (init.method == 'GET') {
      response = await shiosCaches.match<T>(url);
      if (response) {
        return response;
      }
    }
    this.debug(init.headers);
    const options: UniNamespace.RequestOptions = {
      url,
      method: init.method,
      data: init.data,
      header: init.headers,
      timeout: this.timeout,
    };
    this.debug('UniNamespace.RequestOptions', options);
    const res = await new Promise<UniNamespace.RequestSuccessCallbackResult>(
      (resolve, reject) => {
        uni?.request({
          ...options,
          success: (res) => {
            resolve(res);
          },
          fail: (err) => {
            reject(err);
          },
        });
      }
    );
    response = {
      url,
      method: init.method,
      status: res.statusCode,
      ok: res.statusCode < 400,
      headers: res.header,
      data: res.data as T,
    };
    const controllerName = pathname.split(/[/?]/, 1)[0];
    switch (init?.method?.toUpperCase()) {
      case 'GET':
      case 'HEAD':
        (await shiosCaches.open(controllerName)).put(url, response);
        break;
      case 'POST':
        await (
          await shiosCaches.open(controllerName)
        ).delete(url, {
          ignoreSearch: true,
        });
        break;
      case 'PUT':
      case 'DELETE':
        shiosCaches.delete(controllerName);
        break;
      default:
        break;
    }
    return response;
  }
  /**
   * 组合URL
   * @param {string}pathname URL 部分路径
   * @param {URLParams} urlParams path 与 search
   * @returns {string} URL
   */
  CombinedURL(pathname: string, urlParams?: URLParams): string {
    if (pathname.charAt(0) == '/') {
      pathname = pathname.substring(1);
    }
    // 请求的controller
    let url = this.baseUrl + pathname;

    if (urlParams?.path) {
      let path = urlParams.path.toString();
      const joiner = url.charAt(url.length - 1) == '/' ? '' : '/';
      if (path.charAt(0) == '/') {
        path = path.substring(1);
      }

      url += joiner + path;
    }
    let searchString = [
      ...this.getSearchStrings(urlParams?.search),
      ...this.getSearchStrings(urlParams?.query),
    ].join('&');
    if (!!searchString) {
      url += '?' + searchString;
    }
    return url;
  }
  getSearchStrings(searchParams?: SearchParams) {
    let searchStrings: string[] = [];

    if (!searchParams) {
      return searchStrings;
    }

    for (const key in searchParams) {
      // if (Object.prototype.hasOwnProperty.call(searchParams, key)) {
      // }
      let element = searchParams[key] ?? '';
      //let URIComponent: string;
      if (element instanceof Array) {
        element = element.join(',');
      }
      searchStrings.push(`${key}=${encodeURIComponent(element)}`);
    }
    return searchStrings;
  }
  /**
   * 设置请求标头
   * @param {HeadersInit} headersInit - headersInit
   * @return {Headers} header - headers
   */
  setHeaders(headersInit?: Record<string, string>): Record<string, string> {
    // let headers = new Headers(headersInit);
    // if (!headers.has("Content-Type")) {
    //   headers.append("Content-Type", "application/json");
    // }
    // if (this.token && !headers.has("Authorization")) {
    //   headers.append("Authorization", `Bearer ${this.token}`);
    // }

    return Object.assign(this.headers, headersInit); //{ ...this.headers, ...headersInit };
  }
  private index: number = 0;
  private handleResponse(response: Response): unknown {
    if (this.index > this._chain.length - 1) {
      return;
    }
    let middleware = this._chain[this.index];
    this.index++;
    return middleware(response, this.handleResponse.bind(this));
  }
  public use(handle: ChainFunc) {
    this._chain.push(handle);
  }
  // getError(response: Response, next?: ChainFunc) {
  //   if (!response.ok) {
  //     console.error(response);
  //     return response;
  //   }
  //   return next?.(response);
  // }
}
/**
 * 初始化 shios
 * @param {string? } baseUrl - 例：https://example.com
 * - 当baseUrl包含pathname 时,请以 / 结尾 例： https://example.com/api/test/
 * - 当baseUrl为空或为pathname 时,请以 / 结尾, baseUrl 为 window.location.orign 例： 'api/', 结果为 window.location.orign + baseUrl
 * @return { Shios } shios - 实例化Shios
 */
export function useShios(baseUrl?: string): Shios {
  let shios = Shios.getInstance();
  if (baseUrl) {
    // if (!/http[s]?:\/\//.test(baseUrl)) {
    //   throw new SyntaxError('URL 请以 http[s]:// 开头');
    // } else if (
    //   !/^http[s]?:\/\/(\w+(\.\w+){1,3}|localhost)(:\d+)?/.test(baseUrl)
    // ) {
    //   throw new SyntaxError(`${baseUrl} 不是一个URL.`);
    // }
    shios.baseUrl = baseUrl;
    if (!shios.baseUrl.endsWith('/')) {
      shios.baseUrl += '/';
    }
  }

  // if (baseUrl && baseUrl != '/') {
  //   if (baseUrl.startsWith('http')) {
  //     shios.baseUrl = new URL(baseUrl);
  //   } else {
  //     shios.baseUrl.pathname = baseUrl;
  //   }
  // }
  return shios;
}
